LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

socket

2022/4/10

socket通信

socket

socket 被译为“套接字”,它是计算机之间进行通信的一种约定或一种方式。

通过 socket 约定,不同计算机之间可以进行通信

在linux里一切皆文件,socket是一个表示网络连接的文件,对应网络连接的文件描述符。

在windows里,socket是一个网络连接,有别于文件。


流格式套接字

流格式套接字也叫“面向连接的套接字”(Stream Sockets),在代码中使用 SOCK_STREAM 表示。

SOCK_STREAM 是一种可靠的、双向的通信数据流,

数据可以准确无误地到达另一台计算机,如果损坏或丢失,可以重新发送。

流格式套接字使用了 TCP 协议,TCP 协议会控制你的数据按照顺序到达并且没有错误。


TCP

TCP全称The Transmission Control Protocol,传输控制协议

各个标志位及其意义:SYN(建立连接标志位)、ACK(确认标志位)、FIN(释放连接标志位)

TCP建立客户端与服务端的连接需要经过三次握手

第一次:Client将SYN置1,并产生随机数seq=x,发送数据包,Server接受判断SYN为1

第二次:Server将SYN和ACK置1,ack=x+1,seq=y,发送数据包,Client判断ack为x+1,ACK为1

第三次:Client将ACK置1,ack=y+1,发送数据包,Server接受判断ack为y+1,ACK为1

上述过程完成即建立连接


TCP断开客户端与服务端的连接需要经过四次挥手

第一次挥手:Client将FIN置1,发送数据包

第二次挥手:Server接受判断FIN为1,将ACK置1,发送数据包

第三次挥手:Server做好了释放服连接准备,将FIN置1,发送数据包

第四次挥手:Client接受判断FIN为1,将ACK置1,发送数据包

上述过程完成即断开连接


代码实现

代码实现分为客户端(client)和服务端(server)的实现

C++实现

C语言中文网的例程非常清晰,可以直接参考使用

Windows参考:http://c.biancheng.net/view/2129.html

Linux参考:http://c.biancheng.net/view/2128.html


注意事项:

在windows下使用g++编译需要加上-lws2_32选项,例如编译server.cpp如下

g++ server.cpp -o server.exe -lws2_32

如果用该代码与调试助手通信,需要注意端口和地址格式

例如我拿sockit进行调试,地址格式为AF_INET,端口前需要加上htons


Qt实现

Qt支持跨平台,我在windows上实现的效果,可以运用到linux。

最终实现了client界面和server界面编程,并经测试两端可以互通。

client界面
#include "clientwindow.h"
#include "serverwindow.h"

ClientWindow::ClientWindow(QWidget *parent)
    : QMainWindow(parent)
{
    // 由于linux qt版本较低
    // connect 采用旧版写法
    // 即 connect(Button,&QPushButton::clicked,this,&ButtonClicked);
    // 写成 connect(Button,SIGNAL(clicked()),this,SLOT(ButtonClicked()));

    //模式选择
    ChooseClientButton = new QPushButton(this);
    ChooseClientButton->setText("客户端");
    ChooseClientButton->move(0,0);
    ChooseServerButton = new QPushButton(this);
    ChooseServerButton->setText("服务端");
    ChooseServerButton->move(100,0);
    connect(ChooseServerButton,SIGNAL(clicked()),this,SLOT(ChooseServerButtonClicked())); //本身是客户端 所以只绑定服务端按钮

    //Ip输入框
    IpLabel = new QLabel(this);
    IpLabel->setText("Ip:");
    IpLabel->move(70,50);
    IpInput = new QLineEdit(this);
    IpInput->setFixedSize(200,35);
    IpInput->move(100,50);
    IpInput->setPlaceholderText("Ip地址...");
    IpInput->setClearButtonEnabled(true);

    //端口输入端
    PortLabel = new QLabel(this);
    PortLabel->setText("Port:");
    PortLabel->move(50,100);
    PortInput = new QLineEdit(this);
    PortInput->setFixedSize(200,35);
    PortInput->move(100,100);
    PortInput->setPlaceholderText("端口...");
    PortInput->setClearButtonEnabled(true);

    //连接按钮
    ConnectButton = new QPushButton(this);
    ConnectButton->setText("连接");
    ConnectButton->move(50,150);
    connect(ConnectButton,SIGNAL(clicked()),this,SLOT(ConnectButtonClicked()));
    connected = false;

    //断开按钮
    DisconnectButton = new QPushButton(this);
    DisconnectButton->setText("断开");
    DisconnectButton->move(190,150);
    connect(DisconnectButton,SIGNAL(clicked()),this,SLOT(DisconnectButtonClicked()));

    //文本输入框
    TextInput = new QTextEdit(this);
    TextInput->move(50,230);
    TextInput->setFixedSize(250,300);
    TextInputLabel = new QLabel(this);
    TextInputLabel->move(50,200);
    TextInputLabel->setText("发送内容");

    //发送按钮
    SendButton = new QPushButton(this);
    SendButton->move(50,550);
    SendButton->setText("发送");
    SendButton->setFixedSize(250,35);
    connect(SendButton,SIGNAL(clicked()),this,SLOT(SendButtonClicked()));

    //发送记录
    RecordLabel = new QLabel(this);
    RecordLabel->move(550,50);
    RecordLabel->setText("发送记录");
    RecordText = new QTextEdit(this);
    RecordText->move(350,80);
    RecordText->setFixedSize(500,450);
    RecordText->setReadOnly(true);

    //记录所有发送文本
    AllRecordText = QString();

    //client
    socket = new QTcpSocket();

    //清除按钮
    ClearRecordTextButton = new QPushButton(this);
    ClearRecordTextButton->move(350,550);
    ClearRecordTextButton->setText("清空");
    ClearRecordTextButton->setFixedSize(500,35);
    connect(ClearRecordTextButton,SIGNAL(clicked()),this,SLOT(ClearRecordTextButtonClicked()));

}

ClientWindow::~ClientWindow()
{


}


void ClientWindow::ConnectButtonClicked()
{
    //获取ip和端口
    Ip = IpInput->text();
    Port = PortInput->text().toInt();

    //连接
    socket->connectToHost(Ip, Port);
    if(socket->waitForConnected(1000))
    {
        connected = true;
        AllRecordText += "已连接\n";
        IpInput->setReadOnly(true);
        PortInput->setReadOnly(true);
        connect(socket,SIGNAL(readyRead()),this,SLOT(ReadServerData()));// 兼容
    }
    else
    {
        AllRecordText += "连接失败,请重新连接\n";
    }
    RecordText->setText(AllRecordText);


}

void ClientWindow::DisconnectButtonClicked()
{
    //断开连接
    connected = false;
    socket->disconnectFromHost();
    IpInput->setReadOnly(false);
    PortInput->setReadOnly(false);
}

void ClientWindow::SendButtonClicked()
{
    if(connected)
    {
        //多行文本 要按换行符分割 可以用QString::SkipEmptyParts跳过空行
        QStringList TextInputList = \
                TextInput->toPlainText().split(QRegExp("[\r\n]"),QString::SkipEmptyParts);
        for(int i =0;i<TextInputList.size();i++)
        {
            CurrentTime = QDateTime::currentDateTime();
            QString current_date =CurrentTime.toString("hh:mm:ss");

            TextInputList[i] +='\n';
            AllRecordText += "[" + current_date + "] " + \
                             Ip + ":" + QString::number(Port) + ": " + \
                    "-->: " + TextInputList[i];
            socket->write(TextInputList[i].toUtf8());
        }
        RecordText->setText(AllRecordText);
    }
    else
    {
        AllRecordText += "未连接,请连接\n";
        RecordText->setText(AllRecordText);
    }
}

void ClientWindow::ClearRecordTextButtonClicked()
{
    //清空记录
    AllRecordText = QString();
    RecordText->setText(AllRecordText);
}

void ClientWindow::ChooseServerButtonClicked()
{

    ServerWindow *server_window = new ServerWindow;
    server_window->resize(900,600);
    server_window->setFont(QFont("宋体",8));
    server_window->setWindowTitle("Socket TCP 通信");
    this->deleteLater();
    this->close();
    server_window->move(this->pos().x(),this->pos().y());
    server_window->show();

}
void ClientWindow::ReadServerData()
{
    QString ReadData = socket->readAll();
    //多行文本 要按换行符分割 可以用QString::SkipEmptyParts跳过空行
    QStringList TextInputList = \
            ReadData.split(QRegExp("[\r\n]"),QString::SkipEmptyParts);
    for(int i =0;i<TextInputList.size();i++)
    {
        CurrentTime = QDateTime::currentDateTime();
        QString current_date =CurrentTime.toString("hh:mm:ss");

        TextInputList[i] +='\n';
        AllRecordText += "[" + current_date + "] " + \
                         Ip + ":" + QString::number(Port) + ": " + \
                        "<--: " + TextInputList[i];
    }
    RecordText->setText(AllRecordText);
}

效果如下:


server界面

server和client实现类似

#include "serverwindow.h"
#include "clientwindow.h"

ServerWindow::ServerWindow(QWidget *parent)
    : QMainWindow(parent)
{
    //由于linux qt版本较低
    //connect 采用旧版写法
    //即 connect(Button,&QPushButton::clicked,this,&ButtonClicked);
    //写成 connect(Button,SIGNAL(clicked()),this,SLOT(ButtonClicked()));

    //模式选择
    ChooseClientButton = new QPushButton(this);
    ChooseClientButton->setText("客户端");
    ChooseClientButton->move(0,0);
    ChooseServerButton = new QPushButton(this);
    ChooseServerButton->setText("服务端");
    ChooseServerButton->move(100,0);
    connect(ChooseClientButton,SIGNAL(clicked()),this,SLOT(ChooseClientButtonClicked())); //本身是服务端 所以只绑定客户端按钮

    //Ip输入框
    IpLabel = new QLabel(this);
    IpLabel->setText("Ip:");
    IpLabel->move(70,50);
    IpInput = new QLineEdit(this);
    IpInput->setFixedSize(200,35);
    IpInput->move(100,50);
    IpInput->setPlaceholderText("Ip地址...");
    IpInput->setClearButtonEnabled(true);

    //端口输入端
    PortLabel = new QLabel(this);
    PortLabel->setText("Port:");
    PortLabel->move(50,100);
    PortInput = new QLineEdit(this);
    PortInput->setFixedSize(200,35);
    PortInput->move(100,100);
    PortInput->setPlaceholderText("端口...");
    PortInput->setClearButtonEnabled(true);

    //侦听按钮
    ListenButton = new QPushButton(this);
    ListenButton->setText("侦听");
    ListenButton->move(50,150);
    connect(ListenButton,SIGNAL(clicked()),this,SLOT(ListenButtonClicked()));
    listened = false;
    connected = false;

    //取消按钮
    DisListenButton = new QPushButton(this);
    DisListenButton->setText("取消侦听");
    DisListenButton->move(190,150);
    connect(DisListenButton,SIGNAL(clicked()),this,SLOT(DisListenButtonClicked()));

    //文本输入框
    TextInput = new QTextEdit(this);
    TextInput->move(50,230);
    TextInput->setFixedSize(250,300);
    TextInputLabel = new QLabel(this);
    TextInputLabel->move(50,200);
    TextInputLabel->setText("发送内容");

    //发送按钮
    SendButton = new QPushButton(this);
    SendButton->move(50,550);
    SendButton->setText("发送");
    SendButton->setFixedSize(250,35);
    connect(SendButton,SIGNAL(clicked()),this,SLOT(SendButtonClicked()));

    //发送记录
    RecordLabel = new QLabel(this);
    RecordLabel->move(550,50);
    RecordLabel->setText("发送记录");
    RecordText = new QTextEdit(this);
    RecordText->move(350,80);
    RecordText->setFixedSize(500,450);
    RecordText->setReadOnly(true);

    //记录所有发送文本
    AllRecordText = QString();

    //清除按钮
    ClearRecordTextButton = new QPushButton(this);
    ClearRecordTextButton->move(350,550);
    ClearRecordTextButton->setText("清空");
    ClearRecordTextButton->setFixedSize(500,35);
    connect(ClearRecordTextButton,SIGNAL(clicked()),
            this,SLOT(ClearRecordTextButtonClicked()));

}

ServerWindow::~ServerWindow()
{


}


void ServerWindow::ListenButtonClicked()
{
    //获取ip和端口
    Ip = IpInput->text();
    Port = PortInput->text().toInt();

    //进入侦听模式
    server = new QTcpServer(this);
    server->listen(QHostAddress(Ip),Port);
    connect(server,SIGNAL(newConnection()),this,SLOT(NewClientConnect()));
    listened = true;

    //显示侦听
    AllRecordText += "侦听中...\n";
    ListenButton->setEnabled(false);

    //禁止输入
    IpInput->setReadOnly(true);
    PortInput->setReadOnly(true);
    RecordText->setText(AllRecordText);

}

void ServerWindow::ClearRecordTextButtonClicked()
{
    //清空记录
    AllRecordText = QString();
    RecordText->setText(AllRecordText);
}

void ServerWindow::ChooseClientButtonClicked()
{
    //关闭服务端并在同位置显示客户端
    ClientWindow *client_window = new ClientWindow;
    client_window->resize(900,600);
    client_window->setFont(QFont("宋体",8));
    client_window->setWindowTitle("Socket TCP 通信");
    this->deleteLater();
    this->close();
    client_window->move(this->pos().x(),this->pos().y());
    client_window->show();
}

void ServerWindow::NewClientConnect()
{
    //显示
    AllRecordText += "已连接\n";
    RecordText->setText(AllRecordText);
    connected = true;

    //获得套接字并绑定
    socket = new QTcpSocket(this);
    socket = server->nextPendingConnection();
    connect(socket,SIGNAL(readyRead()),this,SLOT(ReadClientData()));
}

void ServerWindow::ReadClientData()
{
    //读取文本 按换行符分割 用QString::SkipEmptyParts跳过空行
    QString ReadData =  socket->readAll();
    QStringList TextInputList = ReadData.split(QRegExp("[\r\n]"),QString::SkipEmptyParts);

    //遍历取出显示内容
    for(int i =0;i<TextInputList.size();i++)
    {
        CurrentTime = QDateTime::currentDateTime();
        QString current_date =CurrentTime.toString("hh:mm:ss");

        TextInputList[i] +='\n';
        AllRecordText += "[" + current_date + "] " + \
                Ip + ":" + QString::number(Port) + ": " + \
                "<--: " + TextInputList[i];
    }

    //显示
    RecordText->setText(AllRecordText);
}

void ServerWindow::DisListenButtonClicked()
{
    if(listened)
    {
        //显示
        AllRecordText += "取消侦听\n";
        RecordText->setText(AllRecordText);

        //取消侦听 close是禁止连接 还是可以通信
        server->deleteLater();
        ListenButton->setEnabled(true);
        IpInput->setReadOnly(false);
        PortInput->setReadOnly(false);
        listened = false;
        connected = false;
    }
}

void ServerWindow::SendButtonClicked()
{
    if(connected)
    {
        //发送文本 要按换行符分割 可以用QString::SkipEmptyParts跳过空行
        QStringList TextInputList = TextInput->toPlainText().split(
                    QRegExp("[\r\n]"),QString::SkipEmptyParts);
        for(int i =0;i<TextInputList.size();i++)
        {
            CurrentTime = QDateTime::currentDateTime();
            QString current_date =CurrentTime.toString("hh:mm:ss");

            TextInputList[i] +='\n';
            AllRecordText += "[" + current_date + "] " + \
                    Ip + ":" + QString::number(Port) + ": " + \
                    "-->: " + TextInputList[i];
            socket->write(TextInputList[i].toUtf8());
        }
        RecordText->setText(AllRecordText);
    }
    else
    {
        AllRecordText += "未连接,请连接\n";
        RecordText->setText(AllRecordText);
    }
}


效果如下:



程序打包

为了让程序可以在多个平台上使用

我将qt程序分别在windows和linux编译成可执行文件,并打包依赖环境

windows打包环境采用qt自带的windeployqt,比较简单

linux打包环境采用需要自己编写脚本文件,实现如下:

1.打包依赖项

编写了一个pack.sh用于打包依赖环境,内容如下

#!/bin/sh
dir=$(pwd) # current dir
liblist=$(ldd $1 | awk '{if (match($3,"/")){ printf("%s ",$3)}}') # get lib list
cp $liblist $dir # copy

通过ldd命令显示依赖环境,结合awk,match匹配获得动态链接库的绝对路径

最后通过cp命令将这些文件拷贝到当前文件夹、

2.运行时指定动态库链接路径

如果更换了一台电脑,它并不会默认当前文件夹为动态链接库搜索路径

所以需要一个与可执行文件同名的的脚本,内容如下

#!/bin/sh
appname=$(basename $0 | cut -d . -f1) 
dirname=$(dirname $0)
LD_LIBRARY_PATH=$dirname
export LD_LIBRARY_PATH
chmod +x $dirname/$appname
$dirname/$appname

程序实现的也就是设置动态链接库为该目录

所以在其他机器上只要运行这个socket.sh即可将可执行文件链接该文件夹的动态链接库并成功执行

3.打包成App

为了获得更加方便的可点击执行的可执行文件,我还编写了getDesktop.sh脚本,内容如下

exe="socketTCP"
file_name=$exe".desktop"
echo "#!/usr/bin/env xdg-open" > $file_name
echo "[Desktop Entry]" >> $file_name
echo "Version=1.0" >> $file_name
echo "Type=Application" >> $file_name
echo "Terminal=false" >> $file_name
echo "Exec="$(pwd)"/"$exe".sh" >> $file_name
echo "Name="$exe >> $file_name
chmod +x $file_name

实现原理就是根据所在路径编写可点击执行的.desktop文件